/*
 * Copyright 2012 OmniFaces.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.omnifaces.eventlistener;

import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.ComponentSystemEvent;
import javax.faces.event.PostValidateEvent;
import javax.faces.event.PreValidateEvent;
import javax.faces.event.SystemEvent;
import javax.faces.event.SystemEventListener;
import javax.faces.validator.BeanValidator;
import javax.faces.validator.Validator;

import java.util.logging.Level;
import java.util.logging.Logger;


/**
 * Overrides {@link javax.faces.validator.BeanValidator#setValidationGroups(String)} for all components in the current view.
 * This allows to temporarily use different validationGroups e. g. if a specific button is clicked.
 * <p>
 * This listener can be disabled in your web.xml:
 * 
 * <pre>
 * &lt;context-param&gt;
 * 	&lt;param-name&gt;org.omnifaces.DISABLE_PARTIAL_VALIDATION&lt;/param-name&gt;
 * 	&lt;param-value&gt;true&lt;/param-value&gt;
 * &lt;/context-param&gt;
 * </pre>
 *
 * @author Adrian Gygax, mimacom ag
 * @since 1.3
 */
public class PartialValidationSystemEventListener implements SystemEventListener {

    /**
     * Context parameter name to disable partial validation.
     */
    public static final String DISABLE_PARTIAL_VALIDATION = "org.omnifaces.DISABLE_PARTIAL_VALIDATION";

    /**
     * The name of the request parameter which contains the validation groups to apply.
     */
    public static final String REQ_PARAM_PARTIAL_VALIDATION_GROUPS = "partialValidationGroups";

    /**
     * Validation groups before they were replaced by this listener.
     */
    private static final String KEY_ORIGINAL_VALIDATE_GROUPS = "originalValidateGroups";

    private static final Logger LOGGER = Logger.getLogger(PartialValidationSystemEventListener.class.getName());

    private Boolean enabled;

    public void processEvent(SystemEvent event) throws AbortProcessingException {
        if (event instanceof PreValidateEvent) {
            handlePreValidate(event);
        } else if (event instanceof PostValidateEvent) {
            handlePostValidate(event);
        }
    }

    /**
     * Replaces the original value of {@link javax.faces.validator.BeanValidator#getValidationGroups()} with the value from the
     * {@link #REQ_PARAM_PARTIAL_VALIDATION_GROUPS} request attribute.
     */
    private void handlePreValidate(SystemEvent event) {
        String validationGroups = FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap()
                .get(REQ_PARAM_PARTIAL_VALIDATION_GROUPS);
        if (validationGroups == null || validationGroups.length() == 0) {
            if(LOGGER.isLoggable(Level.FINER)) {
                LOGGER.finer("No request parameter named '" + REQ_PARAM_PARTIAL_VALIDATION_GROUPS +"' found, validationGroups are not overridden");
            }
            return;
        } else if ("none".equals(validationGroups)) {
            validationGroups = NoValidationGroup.class.getName();
        }

        final ComponentSystemEvent preValidateEvent = (ComponentSystemEvent) event;
        final UIComponent component = preValidateEvent.getComponent();

        final BeanValidator beanValidator = getBeanValidator((EditableValueHolder) component);
        String originalValidationGroups = beanValidator.getValidationGroups();

        FacesContext.getCurrentInstance().getAttributes().put(createOrginialValidateGroupsKey(component), originalValidationGroups);

        beanValidator.setValidationGroups(validationGroups);
        if(LOGGER.isLoggable(Level.FINER)) {
            LOGGER.finer(String.format("Validation groups for component with id '%s' overriden from '%s' to '%s'",
                    component.getClientId(),
                    originalValidationGroups,
                    validationGroups));
        }
    }

    private String createOrginialValidateGroupsKey(UIComponent component) {
        return KEY_ORIGINAL_VALIDATE_GROUPS + component.getClientId();
    }

    /**
     * Restores the original validationGroups value. This is necassary as this property is cached in {@link javax.faces.validator.BeanValidator} and thus would'nt
     * have the correct value on the next request for which the validation is NOT overriden.
     */
    private void handlePostValidate(SystemEvent event) {
        final ComponentSystemEvent preValidateEvent = (ComponentSystemEvent) event;
        final UIComponent component = preValidateEvent.getComponent();
        String originalValidateGroups = (String) FacesContext.getCurrentInstance().getAttributes().get(createOrginialValidateGroupsKey(component));
        if (originalValidateGroups != null) {
            final BeanValidator beanValidator = getBeanValidator((EditableValueHolder) component);

            beanValidator.setValidationGroups(originalValidateGroups);
            if(LOGGER.isLoggable(Level.FINER)) {
                LOGGER.finer(String.format(
                        "Restored original validation groups for component with id '%s' to '%s'",
                        component.getClientId(),
                        originalValidateGroups));
            }
        }
    }

    /**
     * Only listens to components which have a {@link javax.faces.validator.BeanValidator} assigned.
     */
    public boolean isListenerForSource(Object source) {

        return isEnabled() && source instanceof EditableValueHolder && getBeanValidator((EditableValueHolder) source) != null;

    }

    private boolean isEnabled() {
        if (enabled == null) {
            String enabledSetting = FacesContext.getCurrentInstance().getExternalContext().getInitParameter(DISABLE_PARTIAL_VALIDATION);
            enabled = !Boolean.valueOf(enabledSetting);

            if (!enabled) {
                LOGGER.info(
                        "Omnifaces Partial validation disabled. To enable remove context-param '"
                        + DISABLE_PARTIAL_VALIDATION +
                        "' in your web.xml (or set value to 'true'");
            }
        }
        return enabled;
    }

    private BeanValidator getBeanValidator(EditableValueHolder component) {
        Validator[] validators = component.getValidators();
        for (Validator validator : validators) {
            if (validator instanceof BeanValidator) {
                return (BeanValidator) validator;
            }
        }
        return null;
    }

    /**
     * Dummy validation group to disable any validation.
     */
    private static interface NoValidationGroup {

    }

}
